﻿using System;
using System.Collections.Generic;
using System.Text;
using Metro = System.IO.Packaging;
using System.Collections;
using System.Xml;
using System.Reflection;
using System.IO;
using System.Xml.Xsl;
using System.Globalization;

namespace DocX2HtmlConversion
{
    class Program
    {
        #region Argument Handling Data

        private enum ArgumentCode
        {
            Help,
            InputFile,
            OutputFile,
            LogFile,
            ConfigFile
        }

        private struct ArgumentPair
        {
            public ArgumentPair(ArgumentCode c, string s)
            {
                this.argType = c;
                this.argFlagString = s;
            }

            public ArgumentCode argType;
            public string argFlagString;
        }

        private static ArgumentPair[] PossibleArguments = new ArgumentPair[] {
            new ArgumentPair(ArgumentCode.Help, "-?"),
            new ArgumentPair(ArgumentCode.Help, "-help"),
            new ArgumentPair(ArgumentCode.InputFile, "-in"),
            new ArgumentPair(ArgumentCode.OutputFile, "-out"),
            new ArgumentPair(ArgumentCode.LogFile, "-log"),
            new ArgumentPair(ArgumentCode.ConfigFile, "-config"),
        };

        private static Dictionary<ArgumentCode, string> actualArguments = new Dictionary<ArgumentCode, string>();

        #endregion

        /// <summary>
        /// prints out the usage help to the console
        /// </summary>
        static void PrintUsage()
        {
            Console.WriteLine("Usage: {0} -in <inputfilename> -out <outputfilename> [-config <configfilename>] [-log <logfilename>]",
                              Assembly.GetExecutingAssembly().ManifestModule.Name);
        }

        static void Log(string logString)
        {
            if (!String.IsNullOrEmpty(logString) &&
                actualArguments.ContainsKey(ArgumentCode.LogFile) &&
                !String.IsNullOrEmpty(actualArguments[ArgumentCode.LogFile]))
            {
                System.IO.FileInfo fileInfo = new System.IO.FileInfo(actualArguments[ArgumentCode.LogFile]);

                if (fileInfo != null)
                {
                    System.IO.TextWriter applicationLog = null;

                    if (fileInfo.Exists)
                    {
                        applicationLog = fileInfo.AppendText();
                    }
                    else
                    {
                        applicationLog = fileInfo.CreateText();
                    }

                    if (applicationLog != null)
                    {
                        applicationLog.WriteLine(logString);

                        applicationLog.Flush();
                        applicationLog.Close();
                    }
                }
            }

            if (!String.IsNullOrEmpty(logString))
            {
                Console.WriteLine(logString);
            }
        }

        private const string wordDocId = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
        const string RelationShipUri = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";

        const string LinkRelationshipUri = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
        const string ImageRelationshipUri = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
        private const string ValAttrName = "w:val";

        static string[] additionalStreams = new string[]
            { 
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", 
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable",
                "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings",
            };

        static void Main(string[] args)
        {
            #region Argument Handling Code
            // read arguments
            for (int i = 0; i < args.Length; i++)
            {
                bool argumentValid = false;
                foreach (ArgumentPair ap in PossibleArguments)
                {
                    if (String.Compare(ap.argFlagString, args[i], StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        switch (ap.argType)
                        {
                            case ArgumentCode.Help:
                                PrintUsage();
                                return;

                            case ArgumentCode.InputFile:
                            case ArgumentCode.OutputFile:
                            case ArgumentCode.ConfigFile:
                            case ArgumentCode.LogFile:
                                if (i + 1 < args.Length)
                                {
                                    actualArguments[ap.argType] = args[i + 1];
                                    i++;
                                    argumentValid = true;
                                }
                                break;
                        }
                        break;
                    }
                }

                if (!argumentValid)
                {
                    Console.WriteLine("unknown argument {0}", args[i]);
                    PrintUsage();
                    return;
                }
            }

            // validate arguments
            if (!actualArguments.ContainsKey(ArgumentCode.InputFile) ||
                String.IsNullOrEmpty(actualArguments[ArgumentCode.InputFile]) ||
                !actualArguments.ContainsKey(ArgumentCode.OutputFile) ||
                String.IsNullOrEmpty(actualArguments[ArgumentCode.OutputFile]))
            {
                Console.WriteLine("required argument missing");
                PrintUsage();
                return;
            }

            #endregion

            try
            {
                Metro.ZipPackage pack = (Metro.ZipPackage)Metro.ZipPackage.Open(actualArguments[ArgumentCode.InputFile], System.IO.FileMode.Open, System.IO.FileAccess.Read);

                Metro.PackageRelationshipCollection packRels = pack.GetRelationshipsByType(wordDocId);

                // assemble the "WordML" file
                XmlDocument mainDoc = new XmlDocument();

                foreach (Metro.PackageRelationship r in packRels)
                {
                    Uri uri = Metro.PackUriHelper.CreatePartUri(r.TargetUri);
                    Metro.ZipPackagePart wordDoc = (Metro.ZipPackagePart)pack.GetPart(uri);

                    mainDoc.Load(wordDoc.GetStream());

                    XmlNamespaceManager nsm = createNameSpaceManager(mainDoc.NameTable);
                    XmlNode docNode = mainDoc.SelectSingleNode("./w:document", nsm);

                    if (docNode != null)
                    {
                        foreach (string relname in additionalStreams)
                        {
                            Metro.PackageRelationshipCollection wordRels = wordDoc.GetRelationshipsByType(relname);
                            foreach (Metro.PackageRelationship wr in wordRels)
                            {
                                Uri wordPartUri = Metro.PackUriHelper.ResolvePartUri(uri, wr.TargetUri);
                                Metro.ZipPackagePart part = (Metro.ZipPackagePart)pack.GetPart(wordPartUri);
                                XmlDocument partDoc = new XmlDocument();
                                partDoc.Load(part.GetStream());

                                XmlNode partNode = mainDoc.ImportNode(partDoc.DocumentElement, true);
                                if (partNode != null)
                                {
                                    docNode.AppendChild(partNode);
                                }
                            }
                        }
                    }

                    // Load the link relationship tables
                    Hashtable linkTable = new Hashtable();
                    Hashtable imageTable = new Hashtable();

                    Metro.PackageRelationshipCollection linkRels = wordDoc.GetRelationshipsByType(LinkRelationshipUri);
                    Metro.PackageRelationshipCollection imageRels = wordDoc.GetRelationshipsByType(ImageRelationshipUri);

                    ReadRelationshipCollectionIntoHashtable(linkRels, linkTable);
                    ReadRelationshipCollectionIntoHashtable(imageRels, imageTable);

                    System.IO.MemoryStream html = null;

                    // link the hyperlinks from the relationship tables in
                    HandleLinks(mainDoc, nsm, linkTable);
                    HandleImages(mainDoc, nsm, imageTable, linkTable);
                    HandleThemeFonts(mainDoc, nsm);
                    HandleNumberedLists(mainDoc, nsm);

                    // Write output for debugging purposes only
                    //#if WRITE_FINAL_WORDML
                    System.IO.FileStream wml = new System.IO.FileStream(actualArguments[ArgumentCode.OutputFile] + ".xml", System.IO.FileMode.Create);
                    XmlWriter wmlw = XmlWriter.Create(wml);
                    mainDoc.WriteContentTo(wmlw);
                    wmlw.Flush();
                    wmlw.Close();
                    //#endif

                    // Load the proper XSL and apply 
                    System.IO.Stream xslFile = LoadTransformXsl();

                    html = applyXslTransform(mainDoc, xslFile);

                    // write out the HTML.
                    System.IO.FileStream target = new System.IO.FileStream(actualArguments[ArgumentCode.OutputFile], System.IO.FileMode.Create);
                    html.WriteTo(target);
                    target.Flush();
                    break;
                }
            }
            catch (XmlException xex)
            {
                LogInternalError(null, xex);
            }
            catch (XsltException xslEx)
            {
                LogInternalError(null, xslEx);
            }
        }


        private static void ReadRelationshipCollectionIntoHashtable(Metro.PackageRelationshipCollection rels, Hashtable table)
        {
            foreach (Metro.PackageRelationship rel in rels)
            {
                table.Add(rel.Id, rel.TargetUri);
            }
        }

        /// <summary>
        /// count occurrences of numbered lists, save that as a hint on the numbered list node
        /// </summary>
        /// <param name="mainDoc"></param>
        /// <param name="nsm"></param>
        private static void HandleNumberedLists(XmlDocument mainDoc, XmlNamespaceManager nsm)
        {
            // count the number of different list numbering schemes
            XmlNodeList numberNodes = mainDoc.SelectNodes("/w:document/w:numbering/w:num", nsm);

            if (numberNodes.Count == 0)
            {
                return;
            }

            // initialize the abstract number list
            XmlNodeList abstractNumNodes = mainDoc.SelectNodes("/w:document/w:numbering/w:abstractNum", nsm);
            Dictionary<string, AbstractListNumberingDefinition> abstractListDefinitions = new Dictionary<string, AbstractListNumberingDefinition>(abstractNumNodes.Count);
            Dictionary<string, ListNumberingDefinition> instanceListDefinitions = new Dictionary<string, ListNumberingDefinition>(abstractNumNodes.Count);

            // store the abstract list type definitions
            foreach (XmlNode abstractNumNode in abstractNumNodes)
            {
                AbstractListNumberingDefinition absNumDef = new AbstractListNumberingDefinition(abstractNumNode, nsm);

                abstractListDefinitions[absNumDef.ID] = absNumDef;
            }

            // now go through the abstract list definitions and update those that are linked to other styles
            foreach (KeyValuePair<string, AbstractListNumberingDefinition> absNumDef in abstractListDefinitions)
            {
                if (absNumDef.Value.HasLinkedStyle)
                {
                    // find the linked style
                    string linkStyleXPath = "/w:document/w:numbering/w:abstractNum/w:styleLink[@w:val=\"" + absNumDef.Value.LinkedStyleId + "\"]";
                    XmlNode linkedStyleNode = mainDoc.SelectSingleNode(linkStyleXPath, nsm);

                    if (linkedStyleNode != null)
                    {
                        absNumDef.Value.UpdateDefinitionFromLinkedStyle(linkedStyleNode.ParentNode, nsm);
                    }
                }
            }

            // instantiate the list number definitions
            foreach (XmlNode numNode in numberNodes)
            {
                ListNumberingDefinition listDef = new ListNumberingDefinition(numNode, nsm, abstractListDefinitions);

                instanceListDefinitions[listDef.ListNumberId] = listDef;
            }

            XmlNodeList listNodes = mainDoc.SelectNodes("//w:numPr/w:ilvl", nsm);

            foreach (XmlNode node in listNodes)
            {
                string levelId = getAttributeValue(node, ValAttrName);
                XmlNode numIdNode = node.ParentNode.SelectSingleNode("./w:numId", nsm);

                if (!String.IsNullOrEmpty(levelId) && numIdNode != null)
                {
                    string numId = getAttributeValue(numIdNode, ValAttrName);

                    if (!String.IsNullOrEmpty(numId) && instanceListDefinitions.ContainsKey(numId) && instanceListDefinitions[numId].LevelExists(levelId))
                    {
                        XmlAttribute counterAttr = mainDoc.CreateAttribute("numString");

                        instanceListDefinitions[numId].IncrementCounter(levelId);
                        counterAttr.Value = instanceListDefinitions[numId].GetCurrentNumberString(levelId);

                        node.Attributes.Append(counterAttr);

                        string font = instanceListDefinitions[numId].GetFont(levelId);

                        if (!String.IsNullOrEmpty(font))
                        {
                            XmlAttribute fontAttr = mainDoc.CreateAttribute("numFont");

                            fontAttr.Value = font;

                            node.Attributes.Append(fontAttr);
                        }

                        if (instanceListDefinitions[numId].IsBullet(levelId))
                        {
                            XmlAttribute bulletAttr = mainDoc.CreateAttribute("isBullet");

                            bulletAttr.Value = "true";

                            node.Attributes.Append(bulletAttr);
                        }
                    }
                }
            }

        }

        /// <summary>
        /// Calculates the theme font for each run of text and inserts that as a hint
        /// on the run.
        /// </summary>
        /// <param name="mainDoc">
        /// </param>
        /// <param name="nsm">
        /// </param>
        private static void HandleThemeFonts(XmlDocument mainDoc, XmlNamespaceManager nsm)
        {
            //
            // Add theme style to run info 
            //
            XmlNodeList runNodes = mainDoc.SelectNodes("/w:document/w:body//w:r", nsm);

            foreach (XmlNode runNode in runNodes)
            {
                XmlNode runTextNode = runNode.SelectSingleNode("./w:t", nsm);
                if (runTextNode != null)
                {
                    string text = runTextNode.InnerText;
                    if (!string.IsNullOrEmpty(text))
                    {
                        string textClassification = getMsWordLanguageClassification(text);

                        // Add text classification as a hint to the run
                        XmlAttribute classificationAttr = mainDoc.CreateAttribute("textClass");
                        classificationAttr.Value = textClassification;

                        runNode.Attributes.Append(classificationAttr);
                    }
                }
            }
        }

        private const string AsciiText = "asciiText";
        private const string HAnsiText = "hAnsiText";
        private const string EAText = "eaText";
        private const string CSText = "csText";


        /// <summary>
        /// Classifies the string according to the code point of the first character in the string.
        /// In this instance, just assume Ascii
        /// </summary>
        /// <param name="textRun">
        /// </param>
        /// <returns>
        /// </returns>
        private static string getMsWordLanguageClassification(string textRun)
        {
            return HAnsiText;
        }

        private static void HandleLinks(XmlDocument mainDoc, XmlNamespaceManager nsm, Hashtable hyperlinkTable)
        {
            // put the hyperlinks in places
            if (hyperlinkTable != null)
            {
                XmlNodeList linkNodes = mainDoc.SelectNodes("//w:hyperlink", nsm);

                foreach (XmlNode node in linkNodes)
                {
                    // need to convert these three attributes
                    XmlAttribute ridAttr = node.Attributes["r:id"];

                    if (ridAttr != null)
                    {
                        string linkRelationShipId = ridAttr.Value;

                        XmlAttribute linkAttr = mainDoc.CreateAttribute("w:dest", WordMlNamespaceUri);
                        linkAttr.Value = hyperlinkTable[linkRelationShipId] as string;

                        if (linkAttr.Value != null)
                        {
                            linkAttr.Value = getServerRelativePath(srcDocLibPath, linkAttr.Value);

                            XmlNode hlinkNode = mainDoc.CreateNode(XmlNodeType.Element, "w:hlink", WordMlNamespaceUri);

                            hlinkNode.Attributes.Append(linkAttr);

                            // copy the other attributes (such as target, tooltip, ...)
                            foreach (XmlAttribute attribute in node.Attributes)
                            {
                                // you have to clone the attribute, else it messes up the node.Attributes collection enumeration
                                hlinkNode.Attributes.Append((XmlAttribute)attribute.Clone());
                            }

                            hlinkNode.InnerXml = node.InnerXml;

                            node.ParentNode.ReplaceChild(hlinkNode, node);
                        }
                    }
                }
            }
        }

        private static void HandleImages(XmlDocument mainDoc, XmlNamespaceManager nsm, Hashtable imagelinkTable, Hashtable hyperlinkTable)
        {
            // handle E2.0 images
            XmlNodeList oartNodes = mainDoc.SelectNodes("//w:drawing/wp:inline|//w:drawing/wp:anchor", nsm);

            string pictureDataXpathQuery = "./a:graphic/a:graphicData/pic:pic/pic:blipFill";
            foreach (XmlNode node in oartNodes)
            {
                XmlNode pictureDataNode = node.SelectSingleNode(pictureDataXpathQuery, nsm);
                if (pictureDataNode != null)
                {
                    WordXmlPicture picture = new WordXmlPicture();

                    picture.ReadStandardAttributes(pictureDataNode);

                    XmlNode picSizeNode = node.SelectSingleNode("./wp:extent", nsm);

                    if (picSizeNode != null)
                    {
                        picture.ReadSizeAttributes(picSizeNode);
                    }

                    XmlNode picLinkNode = node.SelectSingleNode("./wp:docPr/a:hlinkClick", nsm);

                    if (picLinkNode != null)
                    {
                        string linkRelId = getAttributeValue(picLinkNode, "r:id");

                        if (!String.IsNullOrEmpty(linkRelId) && hyperlinkTable != null)
                        {
                            picture.HlinkReference = hyperlinkTable[linkRelId] as string;
                        }

                        picture.ReadLinkAttributes(picLinkNode);
                    }

                    XmlNode linkDataNode = pictureDataNode.SelectSingleNode("./a:blip", nsm);
                    if (linkDataNode != null)
                    {
                        string imgRelId = getAttributeValue(linkDataNode, "r:link");

                        if (!String.IsNullOrEmpty(imgRelId) && imagelinkTable != null)
                        {
                            picture.Src = imagelinkTable[imgRelId] as string;
                        }

                        // if the relationship isn't found, produce a warning
                        //if (String.IsNullOrEmpty(picture.Src))
                        //{
                        //    this.embeddedPicturesDropped++;
                        //}
                    }

                    XmlElement imageElement = createImageElement(mainDoc, picture, srcDocLibPath);
                    if (imageElement != null)
                    {
                        node.ParentNode.ParentNode.ReplaceChild(imageElement, node.ParentNode);
                    }
                }
            }

            // handle E1.0 images
            XmlNodeList nodes = mainDoc.SelectNodes("//w:pict", nsm);
            string shapeXpathQuery = "./v:shape";
            foreach (XmlNode node in nodes)
            {
                XmlNode shapeNode = node.SelectSingleNode(shapeXpathQuery, nsm);
                if (shapeNode != null)
                {
                    WordXmlPicture picture = new WordXmlPicture();

                    picture.ReadStandardAttributes(shapeNode);

                    XmlNode imageDataNode = shapeNode.SelectSingleNode("./v:imagedata", nsm);
                    if (imageDataNode != null)
                    {
                        string imgRelId = getAttributeValue(imageDataNode, "r:id");

                        if (!String.IsNullOrEmpty(imgRelId) && imagelinkTable != null)
                        {
                            picture.Src = imagelinkTable[imgRelId] as string;
                        }

                        // if the relationship isn't found, produce a warning
                        //if (String.IsNullOrEmpty(picture.Src))
                        //{
                        //    embeddedPicturesDropped++;
                        //}
                        picture.HlinkReference = getAttributeValue(imageDataNode, "href");
                    }

                    XmlElement imageElement = createImageElement(mainDoc, picture, srcDocLibPath);
                    if (imageElement != null)
                    {
                        node.ParentNode.ReplaceChild(imageElement, node);
                    }
                }
            }


        }

        /// <summary>
        /// Write out an error message that's visible to an end user. ULSlog a string and an exception if applicable
        /// </summary>
        /// <param name="ulsLogMsg">optional, unlocalized message that goes to ULS</param>
        /// <param name="ex">optional, goes to ULS</param>
        /// <id guid="445fd243-98d0-4218-bf2b-9a593c1f0ee5" />
        /// <owner alias="ROrleth" />
        public static void LogInternalError(string ulsLogMsg, Exception ex)
        {
            StringBuilder sb = new StringBuilder();
            if (!String.IsNullOrEmpty(ulsLogMsg))
            {
                sb.Append(ulsLogMsg);
            }

            if (ex != null)
            {
                sb.Append(ex.ToString());
            }

            Log(sb.ToString());
        }

        /// <summary>
        /// Write out an error message that's visible to an end user
        /// </summary>
        /// <param name="msg">localized message</param>
        /// <id guid="9e21bfe3-eec7-48a2-92a3-ee3dcf42f476" />
        /// <owner alias="ROrleth" />
        public static void LogError(string msg)
        {
            Log(msg);
        }

        /// <id guid="5eefab6d-1d47-49f1-88d1-10263baaaddc" />
        /// <owner alias="ROrleth" />
        private static System.IO.Stream LoadTransformXsl()
        {
            string assemblyPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().GetName().CodeBase);
            assemblyPath = assemblyPath.ToUpperInvariant();
            const string fileProtocol = "file:\\";
            if (assemblyPath.StartsWith(fileProtocol, StringComparison.OrdinalIgnoreCase))
            {
                assemblyPath = assemblyPath.Remove(0, fileProtocol.Length);
            }

            // Check to see if the file exists.
            string transformFilePath = assemblyPath + "\\DocX2Html.custom.xsl";
            FileInfo customXslFileInfo = new FileInfo(transformFilePath);
            System.IO.Stream stream = null;

            if (customXslFileInfo.Exists)
            {
                stream = new System.IO.FileStream(transformFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
            }
            else
            {
                transformFilePath = assemblyPath + "\\DocX2Html.xslt";

                stream = new System.IO.FileStream(transformFilePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
            }

            return stream;
        }

        private const string WordMlNamespaceUri = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";

        /// <id guid="e44aa1c2-e8bc-474c-8f23-23ed4a13c2db" />
        /// <owner alias="ROrleth" />
        private static XmlNamespaceManager createNameSpaceManager(XmlNameTable nameTable)
        {
            XmlNamespaceManager nameSpaceManager = new XmlNamespaceManager(nameTable);

            nameSpaceManager.AddNamespace("w", WordMlNamespaceUri);

            nameSpaceManager.AddNamespace("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
            nameSpaceManager.AddNamespace("a", "http://schemas.openxmlformats.org/drawingml/2006/main");
            nameSpaceManager.AddNamespace("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture");
            nameSpaceManager.AddNamespace("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing");

            nameSpaceManager.AddNamespace("v", "urn:schemas-microsoft-com:vml");
            nameSpaceManager.AddNamespace("w10", "urn:schemas-microsoft-com:office:word");
            nameSpaceManager.AddNamespace("o", "urn:schemas-microsoft-com:office:office");

            return nameSpaceManager;
        }

        /// <id guid="83783cfe-36bd-4929-ace6-05d7927e0bcc" />
        /// <owner alias="ROrleth" />
        private static System.IO.MemoryStream applyXslTransform(XmlDocument document, System.IO.Stream transform)
        {
            System.Xml.Xsl.XslCompiledTransform xslt = new System.Xml.Xsl.XslCompiledTransform();
            XmlReader styleSheet = new XmlTextReader(transform);
            xslt.Load(styleSheet);

            System.IO.MemoryStream memoryStream = new System.IO.MemoryStream();

            // Execute the transform and output the results to a file.
            xslt.Transform(document, null, memoryStream);

            memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
            return memoryStream;
        }



        private static string srcDocLibPath;
        private static int embeddedPicturesDropped;
        private static UInt64 maxFileLen = 0x01000000; // initialize to 16 MB

        /// <id guid="5c6df911-49d4-444b-9b13-51d5fd861b1f" />
        /// <owner alias="ROrleth" />
        private static void readConfigurationFile()
        {
            //if (!string.IsNullOrEmpty(actualArguments[ArgumentCode.ConfigFile]) && File.Exists(actualArguments[ArgumentCode.ConfigFile]))
            //{
            //    XmlTextReader rdr = new XmlTextReader(actualArguments[ArgumentCode.ConfigFile]);

            //    while (rdr.Read())
            //    {
            //        if (rdr.NodeType == XmlNodeType.Element)
            //        {
            //            switch (rdr.Name)
            //            {
            //                case RcaUtilities.ConverterSettingsElementName:
            //                    this.srcDocLibPath = rdr.GetAttribute(RcaUtilities.SrcDocLibAttributeName);
            //                    if (!this.srcDocLibPath.EndsWith("/", StringComparison.Ordinal))
            //                    {
            //                        this.srcDocLibPath = this.srcDocLibPath + "/";
            //                    }

            //                    UInt64 allocLimit;
            //                    if (UInt64.TryParse(rdr.GetAttribute("AllocationLimitHint"), out allocLimit))
            //                    {
            //                        this.maxFileLen = allocLimit;
            //                    }
            //                    break;
            //            }
            //        }
            //    }
            //}
        }

        private class ListLevel
        {
            /// <summary>
            /// constructor used when abstract list levels are instantiated
            /// </summary>
            public ListLevel(XmlNode levelNode, XmlNamespaceManager nsm)
            {
                this.id = getAttributeValue(levelNode, "w:ilvl");

                XmlNode startValueNode = levelNode.SelectSingleNode("w:start", nsm);

                if (startValueNode != null)
                {
                    string startValueString = getAttributeValue(startValueNode, ValAttrName);

                    if (!String.IsNullOrEmpty(startValueString))
                    {
                        this.startValue = System.Convert.ToUInt32(startValueString, CultureInfo.InvariantCulture) - 1; // as I have to increment counters before each instance (to keep sub-list numbering from advancing too early, set startValue one lower;
                    }

                    this.counter = this.startValue;
                }

                XmlNode levelTextNode = levelNode.SelectSingleNode("w:lvlText", nsm);

                if (levelTextNode != null)
                {
                    this.levelText = getAttributeValue(levelTextNode, ValAttrName);
                }

                XmlNode fontNode = levelNode.SelectSingleNode(".//w:rFonts", nsm);

                if (fontNode != null)
                {
                    this.font = getAttributeValue(fontNode, "w:hAnsi");
                }

                XmlNode enumTypeNode = levelNode.SelectSingleNode("w:numFmt", nsm);

                if (enumTypeNode != null)
                {
                    string type = getAttributeValue(enumTypeNode, ValAttrName);

                    // w:numFmt="bullet" indicates a bulleted list
                    this.isBullet = String.Compare(type, "bullet", StringComparison.OrdinalIgnoreCase) == 0;
                }

            }

            /// <summary>
            /// copy constructor
            /// </summary>
            /// <param name="masterCopy"></param>
            public ListLevel(ListLevel masterCopy)
            {
                this.id = masterCopy.ID;
                this.levelText = masterCopy.LevelText;
                this.startValue = masterCopy.StartValue;
                this.counter = this.startValue;
                this.font = masterCopy.Font;
                this.isBullet = masterCopy.IsBullet;
            }

            /// <summary>
            /// Get overridden values
            /// </summary>
            /// <param name="levelNode"></param>
            /// <param name="nsm"></param>
            public void SetOverrides(XmlNode levelNode, XmlNamespaceManager nsm)
            {
                XmlNode startValueNode = levelNode.SelectSingleNode("w:start", nsm);

                if (startValueNode != null)
                {
                    string startValueString = getAttributeValue(startValueNode, ValAttrName);
                    this.startValue = System.Convert.ToUInt32(startValueString, CultureInfo.InvariantCulture) - 1; // as I have to increment counters before each instance (to keep sub-list numbering from advancing too early, set startValue one lower
                }

                this.counter = this.startValue;

                XmlNode levelTextNode = levelNode.SelectSingleNode("w:lvlText", nsm);

                if (levelTextNode != null)
                {
                    this.levelText = getAttributeValue(levelTextNode, ValAttrName);
                }

                XmlNode fontNode = levelNode.SelectSingleNode("//w:rFonts", nsm);

                if (fontNode != null)
                {
                    this.font = getAttributeValue(fontNode, "w:hAnsi");
                }

                XmlNode enumTypeNode = levelNode.SelectSingleNode("w:numFmt", nsm);

                if (enumTypeNode != null)
                {
                    string type = getAttributeValue(enumTypeNode, ValAttrName);

                    // w:numFmt="bullet" indicates a bulleted list
                    this.isBullet = String.Compare(type, "bullet", StringComparison.OrdinalIgnoreCase) == 0;
                }
            }

            private string id;

            /// <summary>
            /// returns the ID of the level
            /// </summary>
            public string ID
            {
                get
                {
                    return this.id;
                }
            }

            private UInt32 startValue;

            /// <summary>
            /// start value of that level
            /// </summary>
            public UInt32 StartValue
            {
                get
                {
                    return this.startValue;
                }
            }

            private UInt32 counter;

            /// <summary>
            /// returns the current count of list items of that level
            /// </summary>
            public UInt32 CurrentValue
            {
                get
                {
                    return this.counter;
                }
            }

            /// <summary>
            /// increments the current count of list items of that level
            /// </summary>
            public void IncrementCounter()
            {
                this.counter++;
            }

            /// <summary>
            /// resets the counter to the start value
            /// </summary>
            /// <id guid="823b5a3c-7501-4746-8dc4-7b098de5947a" />
            /// <owner alias="ROrleth" />
            public void ResetCounter()
            {
                this.counter = this.startValue;
            }

            private string levelText;

            /// <summary>
            /// returns the indicated lvlText value
            /// </summary>
            public string LevelText
            {
                get
                {
                    return this.levelText;
                }
            }

            private string font;

            /// <summary>
            /// returns the font name
            /// </summary>
            public string Font
            {
                get
                {
                    return this.font;
                }
            }

            private bool isBullet;

            /// <summary>
            /// returns whether the enumeration type is a bulleted list or not
            /// </summary>
            public bool IsBullet
            {
                get
                {
                    return this.isBullet;
                }
            }
        }

        /// <summary>
        /// private helper class to deal with abstract number lists
        /// </summary>
        private class AbstractListNumberingDefinition
        {
            private Dictionary<string, ListLevel> listLevels;

            /// <summary>
            /// constructor
            /// </summary>
            /// <param name="abstractNumNode"></param>
            /// <param name="nsm"></param>
            public AbstractListNumberingDefinition(XmlNode abstractNumNode, XmlNamespaceManager nsm)
            {
                string abstractNumString = getAttributeValue(abstractNumNode, "w:abstractNumId");

                if (!String.IsNullOrEmpty(abstractNumString))
                {
                    this.abstractNumDefId = abstractNumString;

                    this.readListLevelsFromAbsNode(abstractNumNode, nsm);

                    // find out whether there is a linked abstractNum definition that this needs to be populated from later on
                    XmlNode linkedStyleNode = abstractNumNode.SelectSingleNode("./w:numStyleLink", nsm);

                    if (linkedStyleNode != null)
                    {
                        this.linkedStyleId = getAttributeValue(linkedStyleNode, ValAttrName);
                    }
                }
            }

            /// <summary>
            /// update the level definitions from a linked abstractNum node
            /// </summary>
            /// <param name="linkedNode">
            /// </param>
            /// <param name="nsm">
            /// </param>
            /// <id guid="36473168-7947-41ea-8210-839bf07eded7" />
            /// <owner alias="ROrleth" />
            public void UpdateDefinitionFromLinkedStyle(XmlNode linkedNode, XmlNamespaceManager nsm)
            {
                if (!this.HasLinkedStyle)
                    return;

                this.readListLevelsFromAbsNode(linkedNode, nsm);
            }

            /// <id guid="0e05c34c-f257-4c76-8916-3059af84e333" />
            /// <owner alias="ROrleth" />
            private void readListLevelsFromAbsNode(XmlNode absNumNode, XmlNamespaceManager nsm)
            {
                XmlNodeList levelNodes = absNumNode.SelectNodes("./w:lvl", nsm);

                if (this.listLevels == null)
                {
                    this.listLevels = new Dictionary<string, ListLevel>(levelNodes.Count);
                }

                // loop through the levels it defines and instantiate those
                foreach (XmlNode levelNode in levelNodes)
                {
                    ListLevel level = new ListLevel(levelNode, nsm);

                    this.listLevels[level.ID] = level;
                }
            }

            private string linkedStyleId;

            /// <summary>
            /// returnts the ID of the linked style
            /// </summary>
            /// <id guid="ae2caeec-2d86-4e5f-b816-d508f6f2c893" />
            /// <owner alias="ROrleth" />
            public string LinkedStyleId
            {
                get
                {
                    return this.linkedStyleId;
                }
            }

            /// <summary>
            /// indicates whether there is a linked style
            /// </summary>
            /// <id guid="75d74788-9839-448e-ae23-02d40e013d98" />
            /// <owner alias="ROrleth" />
            public bool HasLinkedStyle
            {
                get
                {
                    return !String.IsNullOrEmpty(this.linkedStyleId);
                }
            }


            private string abstractNumDefId;

            /// <summary>
            /// returns the ID of this abstract number list definition
            /// </summary>
            public string ID
            {
                get
                {
                    return this.abstractNumDefId;
                }
            }

            public Dictionary<String, ListLevel> ListLevels
            {
                get
                {
                    return this.listLevels;
                }
            }

            public int LevelCount
            {
                get
                {
                    if (this.ListLevels != null)
                        return this.listLevels.Count;
                    else
                        return 0;
                }
            }
        }

        /// <summary>
        /// private helper class to deal with number lists
        /// </summary>
        private class ListNumberingDefinition
        {
            /// <summary>
            /// constructor
            /// </summary>
            /// <param name="numNode"></param>
            /// <param name="nsm"></param>
            /// <param name="abstractListDefinitions"></param>
            public ListNumberingDefinition(XmlNode numNode, XmlNamespaceManager nsm, Dictionary<string, AbstractListNumberingDefinition> abstractListDefinitions)
            {
                this.listNumberId = getAttributeValue(numNode, "w:numId");

                XmlNode abstractNumNode = numNode.SelectSingleNode("./w:abstractNumId", nsm);

                if (abstractNumNode != null)
                {
                    this.abstractListDefinition = abstractListDefinitions[getAttributeValue(abstractNumNode, ValAttrName)];

                    this.levels = new Dictionary<string, ListLevel>(this.abstractListDefinition.LevelCount);

                    // initialize the levels to the same as the template ("abstract") list level
                    foreach (KeyValuePair<string, ListLevel> levelEntry in this.abstractListDefinition.ListLevels)
                    {
                        this.levels[levelEntry.Key] = new ListLevel(levelEntry.Value);
                    }

                    // propagate the level overrides into the current list number level definition
                    XmlNodeList levelOverrideNodes = numNode.SelectNodes("./w:lvlOverride", nsm);

                    if (levelOverrideNodes != null)
                    {
                        foreach (XmlNode overrideNode in levelOverrideNodes)
                        {
                            XmlNode node = overrideNode.SelectSingleNode("./w:lvl", nsm);
                            if (node != null)
                            {
                                string overrideLevelId = getAttributeValue(node, "w:ilvl");

                                if (!String.IsNullOrEmpty(overrideLevelId))
                                {
                                    this.levels[overrideLevelId].SetOverrides(node, nsm);
                                }
                            }
                        }
                    }
                }
            }

            private AbstractListNumberingDefinition abstractListDefinition;
            private Dictionary<string, ListLevel> levels;

            /// <summary>
            /// increment the occurrence count of the specified level, reset the occurrence count of derived levels
            /// </summary>
            /// <param name="level"></param>
            public void IncrementCounter(string level)
            {
                this.levels[level].IncrementCounter();

                // here's a bit where the decision to use strings as level IDs was bad - I need to loop through the derived levels and reset their counters
                UInt32 levelNumber = System.Convert.ToUInt32(level, CultureInfo.InvariantCulture) + 1;
                string levelString = levelNumber.ToString(CultureInfo.InvariantCulture);

                while (this.levels.ContainsKey(levelString))
                {
                    this.levels[levelString].ResetCounter();
                    levelNumber++;
                    levelString = levelNumber.ToString(CultureInfo.InvariantCulture);
                }
            }

            private string listNumberId;

            /// <summary>
            /// numId of this list numbering schema
            /// </summary>
            public string ListNumberId
            {
                get
                {
                    return this.listNumberId;
                }
            }

            /// <summary>
            /// returns a string containing the current state of the counters, up to the indicated level
            /// </summary>
            /// <param name="level"></param>
            /// <returns></returns>
            public string GetCurrentNumberString(string level)
            {
                string formatString = this.levels[level].LevelText;
                StringBuilder result = new StringBuilder();
                string temp = string.Empty;

                for (int i = 0; i < formatString.Length; i++)
                {
                    temp = formatString.Substring(i, 1);

                    if (String.CompareOrdinal(temp, "%") == 0)
                    {
                        if (i < formatString.Length - 1)
                        {
                            string formatStringLevel = formatString.Substring(i + 1, 1);
                            // as it turns out, in the format string, the level is 1-based
                            UInt32 levelId = System.Convert.ToUInt32(formatStringLevel, CultureInfo.InvariantCulture) - 1;
                            result.Append(this.levels[levelId.ToString(CultureInfo.InvariantCulture)].CurrentValue.ToString(CultureInfo.InvariantCulture));
                            i++;
                        }
                    }
                    else
                    {
                        result.Append(temp);
                    }
                }

                return result.ToString();
            }

            /// <summary>
            /// retrieve the font name that was specified for the list string
            /// </summary>
            /// <param name="level"></param>
            /// <returns></returns>
            public string GetFont(string level)
            {
                return this.levels[level].Font;
            }

            /// <summary>
            /// retrieve whether the level was a bullet list type
            /// </summary>
            /// <param name="level"></param>
            /// <returns></returns>
            public bool IsBullet(string level)
            {
                return this.levels[level].IsBullet;
            }

            /// <summary>
            /// returns whether the specific level ID exists - in testing we've seen some referential integrity issues due to Word bugs
            /// </summary>
            /// <param name="level">
            /// </param>
            /// <returns>
            /// </returns>
            /// <id guid="b94c13b8-7273-4f6a-927b-178d685fbe0f" />
            /// <owner alias="ROrleth" />
            public bool LevelExists(string level)
            {
                return this.levels.ContainsKey(level);
            }
        }

        internal static string getAttributeValue(XmlNode node, string name)
        {
            string value = string.Empty;

            XmlAttribute attribute = node.Attributes[name];
            if (attribute != null && attribute.Value != null)
            {
                value = attribute.Value;
            }

            return value;
        }

        /// <id guid="5a57dca1-2cfb-4ec0-8c66-4f964787450f" />
        /// <owner alias="ROrleth" />
        internal static string getServerRelativePath(string docLibPath, string itemUrl)
        {
            if (itemUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase) ||
                itemUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
                itemUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
            {
                return itemUrl;
            }
            else
            {
                return docLibPath + itemUrl;
            }
        }
        /// <id guid="100b714f-5397-4420-958b-e03c2d021f7c" />
        /// <owner alias="ROrleth" />
        internal static XmlElement createImageElement(XmlDocument document, WordXmlPicture picture, string fixupPath)
        {
            XmlElement imageElement = null, linkElement = null;

            if (document != null && picture != null)
            {
                imageElement = document.CreateElement("img");

                if (!string.IsNullOrEmpty(picture.Src))
                {
                    imageElement.SetAttribute("src", getServerRelativePath(fixupPath, picture.Src));
                }

                if (!string.IsNullOrEmpty(picture.ID))
                {
                    imageElement.SetAttribute("id", picture.ID);
                }

                if (!string.IsNullOrEmpty(picture.Alt))
                {
                    imageElement.SetAttribute("alt", picture.Alt);
                }

                if (!string.IsNullOrEmpty(picture.Style))
                {
                    imageElement.SetAttribute("style", picture.Style);
                }

                if (picture.WidthSet)
                {
                    imageElement.SetAttribute("width", picture.Width.ToString(CultureInfo.InvariantCulture));
                }

                if (picture.HeightSet)
                {
                    imageElement.SetAttribute("height", picture.Height.ToString(CultureInfo.InvariantCulture));
                }

                if (!String.IsNullOrEmpty(picture.HlinkReference))
                {
                    linkElement = document.CreateElement("a");

                    linkElement.SetAttribute("href", picture.HlinkReference);

                    if (!String.IsNullOrEmpty(picture.TargetFrame))
                    {
                        linkElement.SetAttribute("target", picture.TargetFrame);
                    }

                    if (!String.IsNullOrEmpty(picture.Tooltip))
                    {
                        linkElement.SetAttribute("title", picture.Tooltip);
                    }

                    linkElement.AppendChild(imageElement);

                    imageElement = linkElement;
                }
            }

            return imageElement;
        }
    }

    /// <summary>
    /// Internal helper class for working with Word Document conversions.
    /// </summary>
    /// <id guid="1660a411-54c2-4d53-8631-a7a2b703fcb8" />
    /// <owner alias="ROrleth" />
    internal class WordXmlPicture
    {
        /// <id guid="233b126d-66d0-476e-bcd1-ce30bdc3e65b" />
        /// <owner alias="ROrleth" />
        internal void ReadStandardAttributes(XmlNode fromNode)
        {
            this.ID = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "id");
            this.Type = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "type");
            this.Alt = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "alt");
            this.Style = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "style");
        }

        /// <id guid="048da999-6fbe-41b9-9639-de0e084f3da3" />
        /// <owner alias="ROrleth" />
        internal void ReadLinkAttributes(XmlNode fromNode)
        {
            this.TargetFrame = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "tgtFrame");
            this.Tooltip = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "tooltip");
        }

        private const UInt32 ExtentToPixelConversionFactor = 12700;

        /// <id guid="cb8dfd67-57bb-4ebc-af9d-f6062d25b9ba" />
        /// <owner alias="ROrleth" />
        internal void ReadSizeAttributes(XmlNode fromNode)
        {
            string temp = null;
            temp = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "cx");
            if (!String.IsNullOrEmpty(temp))
            {
                this.Width = Convert.ToUInt32(temp, CultureInfo.InvariantCulture) / ExtentToPixelConversionFactor;
            }
            temp = DocX2HtmlConversion.Program.getAttributeValue(fromNode, "cy");
            if (!String.IsNullOrEmpty(temp))
            {
                this.Height = Convert.ToUInt32(temp, CultureInfo.InvariantCulture) / ExtentToPixelConversionFactor;
            }

        }

        private UInt32 width;
        private bool widthSet;

        /// <summary>
        /// Width in pixels
        /// </summary>
        /// <id guid="f01d0577-7f05-4c1b-8dcf-ad36b93bbc3c" />
        /// <owner alias="ROrleth" />
        public UInt32 Width
        {
            get
            {
                return this.width;
            }
            set
            {
                this.widthSet = true;
                this.width = value;
            }
        }

        /// <summary>
        /// WidthSet - returns true if the width has been set intentionally
        /// </summary>
        /// <id guid="d3cb7ab3-a36a-455f-90aa-539904f2781e" />
        /// <owner alias="ROrleth" />
        public bool WidthSet
        {
            get
            {
                return this.widthSet;
            }
        }

        private UInt32 height;
        private bool heightSet;

        /// <summary>
        /// Height in pixels
        /// </summary>
        /// <id guid="8a499702-53a6-430d-b0d1-7ef10e7711f1" />
        /// <owner alias="ROrleth" />
        public UInt32 Height
        {
            get
            {
                return this.height;
            }
            set
            {
                this.heightSet = true;
                this.height = value;
            }
        }

        /// <summary>
        /// HeightSet - returns true if the height has been set intentionally
        /// </summary>
        /// <id guid="ad9b2c47-ce49-4104-97c1-8fe130d40fcd" />
        /// <owner alias="ROrleth" />
        public bool HeightSet
        {
            get
            {
                return this.heightSet;
            }
        }

        private string targetFrame;

        /// <summary>
        /// Target frame property
        /// </summary>
        /// <id guid="1acdad51-bba9-4876-9c53-b0753094c3e9" />
        /// <owner alias="ROrleth" />
        public string TargetFrame
        {
            get
            {
                return this.targetFrame;
            }
            set
            {
                this.targetFrame = value;
            }
        }

        private string tooltip;

        /// <summary>
        /// tooltip property
        /// </summary>
        /// <id guid="c7b612aa-9970-49be-9569-44c62e4d1aa5" />
        /// <owner alias="ROrleth" />
        public string Tooltip
        {
            get
            {
                return this.tooltip;
            }
            set
            {
                this.tooltip = value;
            }
        }

        private string hlinkRef;

        /// <summary>
        /// store the hyperlink that the picture points to, if applicable
        /// </summary>
        /// <id guid="862f74dc-b0a2-44b9-8d0c-4c6d78abaeca" />
        /// <owner alias="ROrleth" />
        public string HlinkReference
        {
            get
            {
                return this.hlinkRef;
            }
            set
            {
                this.hlinkRef = value;
            }
        }

        private string alt;
        /// <summary>
        /// The attribute of the v:shape node which maps to the
        /// 'alt' attribute of and HTML 'img' tag.
        /// </summary>
        /// <remarks>
        /// Also known as the 'alternate text' property of an
        /// HTML image.
        /// </remarks>
        /// <value>
        /// </value>
        /// <id guid="712de8d5-b603-4c01-a231-183c9de68db5" />
        /// <owner alias="ROrleth" />
        public string Alt
        {
            get
            {
                return this.alt;
            }
            set
            {
                this.alt = value;
            }
        }

        private byte[] data;
        /// <summary>
        /// The decoded data from the coresponding 'w:bindata'
        /// node of the Word Document.
        /// </summary>
        /// <remarks>
        /// This property is set by the conversion process.
        /// </remarks>
        /// <value>
        /// </value>
        /// <id guid="130108bf-d980-4753-b674-4d489acf485c" />
        /// <owner alias="ROrleth" />
        public byte[] Data
        {
            get
            {
                return this.data;
            }
            set
            {
                this.data = value;
            }
        }

        private string id;
        /// <summary>
        /// The identifier of the picture unique only within the scope of
        /// the Word Document.
        /// </summary>
        /// <value>
        /// </value>
        /// <id guid="e0d6cf93-79f7-4a38-884c-6b494b244664" />
        /// <owner alias="ROrleth" />
        public string ID
        {
            get
            {
                return this.id;
            }
            set
            {
                this.id = value;
            }
        }

        private string src;
        /// <summary>
        /// The attribute of the 'v:image' node in the Word
        /// Document which coresponds to the 'w:name' attribute of
        /// the 'w:bindata' node.
        /// </summary>
        /// <remarks>
        /// This is what ties the picture reference to the image data.
        /// </remarks>
        /// <value>
        /// </value>
        /// <id guid="bacbce3f-8a98-46a7-9dcf-049acfc3c1b3" />
        /// <owner alias="ROrleth" />
        public string Src
        {
            get
            {
                return this.src;
            }
            set
            {
                this.src = value;
            }
        }

        private string style;
        /// <summary>
        /// The attribute of the v:shape node which maps to the
        /// 'style' attribute of and HTML 'img' tag.
        /// </summary>
        /// <value>
        /// </value>
        /// <id guid="700b62da-d914-4a40-aa96-1437d2b314e1" />
        /// <owner alias="ROrleth" />
        public string Style
        {
            get
            {
                return this.style;
            }
            set
            {
                this.style = value;
            }
        }

        private string type;
        /// <summary>
        /// The type of the picture as specified by the attribute of the v:shape node
        /// within the Word Document.
        /// </summary>
        /// <remarks>
        /// This value is used as an identifier for a v:type node, which used to specify
        /// properties of the picture within the Word Document.
        /// </remarks>
        /// <value>
        /// </value>
        /// <id guid="78bf5c95-1d55-423c-bc34-92d926203e83" />
        /// <owner alias="ROrleth" />
        public string Type
        {
            get
            {
                return this.type;
            }
            set
            {
                this.type = value;
            }
        }
    }
}


